package resources

import (
	"context"
	"encoding/base64"
	"strconv"
	"testing"

	"github.com/stackrox/rox/generated/internalapi/central"
	"github.com/stackrox/rox/generated/storage"
	"github.com/stackrox/rox/pkg/env"
	"github.com/stackrox/rox/pkg/features"
	"github.com/stackrox/rox/pkg/openshift"
	"github.com/stackrox/rox/pkg/registries/types"
	"github.com/stackrox/rox/pkg/testutils"
	"github.com/stackrox/rox/sensor/common/registry"
	"github.com/stackrox/rox/sensor/kubernetes/eventpipeline/component"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/yaml"
)

var (
	clusterID                      = "cluster-id"
	openshift311DockerConfigSecret = &v1.Secret{
		TypeMeta: metav1.TypeMeta{
			Kind:       "Secret",
			APIVersion: "v1",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:      "default-dockercfg-6167c",
			Namespace: "test-ns",
			Annotations: map[string]string{
				"kubernetes.io/service-account.name": "default",
			},
		},
		Data: map[string][]byte{
			".dockercfg": []byte(`
{
  "docker-registry.default.svc.cluster.local:5000": {
    "username": "serviceaccount",
    "password": "password",
    "email": "serviceaccount@example.org"
  }
}`),
		},
		Type: "kubernetes.io/dockercfg",
	}

	openshift4xDockerConfigSecret = &v1.Secret{
		TypeMeta: metav1.TypeMeta{
			Kind:       "Secret",
			APIVersion: "v1",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:      "default-dockercfg-9w5gn",
			Namespace: "test-ns",
			Annotations: map[string]string{
				"kubernetes.io/service-account.name": "default",
			},
		},
		Data: map[string][]byte{
			".dockercfg": []byte(`
{
  "image-registry.openshift-image-registry.svc:5000": {
    "username": "serviceaccount",
    "password": "password",
    "email": "serviceaccount@example.org"
  }
}`),
		},
		Type: "kubernetes.io/dockercfg",
	}
)

// alwaysInsecureCheckTLS is an implementation of registry.CheckTLS
// which always says the given address is insecure.
func alwaysInsecureCheckTLS(_ context.Context, _ string) (bool, error) {
	return false, nil
}

func TestOpenShiftRegistrySecret_311(t *testing.T) {
	testutils.MustUpdateFeature(t, features.SensorPullSecretsByName, false)
	t.Setenv(env.LocalImageScanningEnabled.EnvVar(), "true")

	regStore := registry.NewRegistryStore(alwaysInsecureCheckTLS)
	d := newSecretDispatcher(clusterID, regStore)

	_ = d.ProcessEvent(openshift311DockerConfigSecret, nil, central.ResourceAction_CREATE_RESOURCE)

	imgName := &storage.ImageName{
		Registry: "docker-registry.default.svc.cluster.local:5000",
		Remote:   "dummy/nginx",
		Tag:      "1.18.0",
		FullName: "docker-registry.default.svc.cluster.local:5000/stackrox/nginx:1.18.0",
	}

	regs, err := regStore.GetPullSecretRegistries(imgName, "dummy", nil)
	assert.Nil(t, regs)
	assert.Error(t, err)

	imgName = &storage.ImageName{
		Registry: "docker-registry.default.svc.cluster.local:5000",
		Remote:   "test-ns/nginx",
		Tag:      "1.18.0",
		FullName: "docker-registry.default.svc.cluster.local:5000/stackrox/nginx:1.18.0",
	}

	regs, err = regStore.GetPullSecretRegistries(imgName, "test-ns", nil)
	assert.NotNil(t, regs)
	assert.NoError(t, err)

	expectedRegConfig := &types.Config{
		Username:         "serviceaccount",
		Password:         "password",
		URL:              "https://docker-registry.default.svc.cluster.local:5000",
		RegistryHostname: "docker-registry.default.svc.cluster.local:5000",
		Autogenerated:    false,
	}

	ctx := context.Background()
	require.Len(t, regs, 1)
	assert.Equal(t, expectedRegConfig.RegistryHostname, regs[0].Config(ctx).RegistryHostname)
	assert.Equal(t, expectedRegConfig, regs[0].Config(ctx))
}

func TestOpenShiftRegistrySecret_4x(t *testing.T) {
	testutils.MustUpdateFeature(t, features.SensorPullSecretsByName, false)
	t.Setenv(env.LocalImageScanningEnabled.EnvVar(), "true")

	regStore := registry.NewRegistryStore(alwaysInsecureCheckTLS)
	d := newSecretDispatcher(clusterID, regStore)

	_ = d.ProcessEvent(openshift4xDockerConfigSecret, nil, central.ResourceAction_CREATE_RESOURCE)

	imgName := &storage.ImageName{
		Registry: "image-registry.openshift-image-registry.svc:5000",
		Remote:   "dummy/nginx",
		Tag:      "1.18.0",
		FullName: "image-registry.openshift-image-registry.svc:5000/stackrox/nginx:1.18.0",
	}

	regs, err := regStore.GetPullSecretRegistries(imgName, "dummy", nil)
	assert.Nil(t, regs)
	assert.Error(t, err)

	imgName = &storage.ImageName{
		Registry: "image-registry.openshift-image-registry.svc:5000",
		Remote:   "test-ns/nginx",
		Tag:      "1.18.0",
		FullName: "image-registry.openshift-image-registry.svc:5000/stackrox/nginx:1.18.0",
	}

	regs, err = regStore.GetPullSecretRegistries(imgName, "test-ns", nil)
	assert.NotNil(t, regs)
	assert.NoError(t, err)

	expectedRegConfig := &types.Config{
		Username:         "serviceaccount",
		Password:         "password",
		URL:              "https://image-registry.openshift-image-registry.svc:5000",
		RegistryHostname: "image-registry.openshift-image-registry.svc:5000",
		Autogenerated:    false,
	}

	ctx := context.Background()
	require.Len(t, regs, 1)
	assert.Equal(t, expectedRegConfig.RegistryHostname, regs[0].Config(ctx).RegistryHostname)
	assert.Equal(t, expectedRegConfig, regs[0].Config(ctx))
}

// TestForceLocalScanning tests that dockerconfig secrets are stored
// in the regStore as expected when local scanning is forced
func TestForceLocalScanning(t *testing.T) {
	testutils.MustUpdateFeature(t, features.SensorPullSecretsByName, false)

	fakeNamespace := "fake-namespace"
	dockerConfigSecret := &v1.Secret{
		TypeMeta:   metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
		ObjectMeta: metav1.ObjectMeta{Name: "fake-secret", Namespace: fakeNamespace},
		Type:       v1.SecretTypeDockercfg,
		Data: map[string][]byte{v1.DockerConfigKey: []byte(`
			{
				"fake.reg.local": {
					"username": "hello",
					"password": "world",
					"email": "hello@example.com"
				}
			}
		`)},
	}
	fakeImage := &storage.ImageName{
		Registry: "fake.reg.local",
		Remote:   "fake/repo",
		Tag:      "latest",
		FullName: "fake.reg.local/fake/repo:latest",
	}

	t.Setenv(env.LocalImageScanningEnabled.EnvVar(), "false")
	t.Setenv(env.DelegatedScanningDisabled.EnvVar(), "false")

	// with feature disabled, registry secret should NOT be stored
	regStore := registry.NewRegistryStore(alwaysInsecureCheckTLS)
	d := newSecretDispatcher(clusterID, regStore)

	d.ProcessEvent(dockerConfigSecret, nil, central.ResourceAction_CREATE_RESOURCE)
	regs, err := regStore.GetPullSecretRegistries(fakeImage, fakeNamespace, nil)
	assert.Nil(t, regs)
	assert.Error(t, err)

	t.Setenv(env.LocalImageScanningEnabled.EnvVar(), "true")
	t.Setenv(env.DelegatedScanningDisabled.EnvVar(), "true")

	// with delegated scanning disabled, registry secret should NOT be stored
	regStore = registry.NewRegistryStore(alwaysInsecureCheckTLS)
	d = newSecretDispatcher(clusterID, regStore)

	d.ProcessEvent(dockerConfigSecret, nil, central.ResourceAction_CREATE_RESOURCE)
	regs, err = regStore.GetPullSecretRegistries(fakeImage, fakeNamespace, nil)
	assert.Nil(t, regs)
	assert.Error(t, err)

	t.Setenv(env.LocalImageScanningEnabled.EnvVar(), "true")
	t.Setenv(env.DelegatedScanningDisabled.EnvVar(), "false")

	// feature is enabled, registry secret should be stored
	d.ProcessEvent(dockerConfigSecret, nil, central.ResourceAction_CREATE_RESOURCE)
	regs, err = regStore.GetPullSecretRegistries(fakeImage, fakeNamespace, nil)
	assert.NotNil(t, regs)
	assert.NoError(t, err)
	require.Len(t, regs, 1)
	assert.Equal(t, regs[0].Config(context.Background()).Username, "hello")

	regStore = registry.NewRegistryStore(alwaysInsecureCheckTLS)
	d = newSecretDispatcher(clusterID, regStore)

	// secrets with an k8s service-account.name other than default should not be stored
	dockerConfigSecret.Annotations = map[string]string{ocpSAAnnotations[0]: "something"}
	d.ProcessEvent(dockerConfigSecret, nil, central.ResourceAction_CREATE_RESOURCE)
	regs, err = regStore.GetPullSecretRegistries(fakeImage, fakeNamespace, nil)
	assert.False(t, regStore.IsLocal(fakeImage))
	assert.Nil(t, regs)
	assert.Error(t, err)

	// secrets with an OCP internal-registry-auth-token.service-account other than default should not be stored
	dockerConfigSecret.Annotations = map[string]string{ocpSAAnnotations[1]: "something"}
	d.ProcessEvent(dockerConfigSecret, nil, central.ResourceAction_CREATE_RESOURCE)
	regs, err = regStore.GetPullSecretRegistries(fakeImage, fakeNamespace, nil)
	assert.False(t, regStore.IsLocal(fakeImage))
	assert.Nil(t, regs)
	assert.Error(t, err)

	// secrets with an saAnnotation of `default` should still be stored
	dockerConfigSecret.Annotations = map[string]string{ocpSAAnnotations[0]: "default"}
	regStore.Cleanup()
	d.ProcessEvent(dockerConfigSecret, nil, central.ResourceAction_CREATE_RESOURCE)
	regs, err = regStore.GetPullSecretRegistries(fakeImage, fakeNamespace, nil)
	assert.True(t, regStore.IsLocal(fakeImage))
	assert.NotNil(t, regs)
	assert.NoError(t, err)

	dockerConfigSecret.Annotations = map[string]string{ocpSAAnnotations[1]: "default"}
	regStore.Cleanup()
	d.ProcessEvent(dockerConfigSecret, nil, central.ResourceAction_CREATE_RESOURCE)
	regs, err = regStore.GetPullSecretRegistries(fakeImage, fakeNamespace, nil)
	assert.True(t, regStore.IsLocal(fakeImage))
	assert.NotNil(t, regs)
	assert.NoError(t, err)
}

// Verifies that secrets are deleted from the registry store when a
// secret is deleted in k8s.
func TestLocalScanningSecretDelete(t *testing.T) {
	testutils.MustUpdateFeature(t, features.SensorPullSecretsByName, true)
	t.Setenv(env.LocalImageScanningEnabled.EnvVar(), "true")
	t.Setenv(env.DelegatedScanningDisabled.EnvVar(), "false")

	fakeNamespace := "fake-namespace"

	dockerConfigSecret := &v1.Secret{
		TypeMeta:   metav1.TypeMeta{Kind: "Secret", APIVersion: "v1"},
		ObjectMeta: metav1.ObjectMeta{Name: "fake-secret", Namespace: fakeNamespace},
		Type:       v1.SecretTypeDockercfg,
		Data: map[string][]byte{v1.DockerConfigKey: []byte(`
			{
				"fake.reg.local": {
					"username": "hello",
					"password": "world",
					"email": "hello@example.com"
				}
			}
		`)},
	}
	fakeImage := &storage.ImageName{
		Registry: "fake.reg.local",
		Remote:   "fake/repo",
		Tag:      "latest",
		FullName: "fake.reg.local/fake/repo:latest",
	}

	regStore := registry.NewRegistryStore(alwaysInsecureCheckTLS)
	d := newSecretDispatcher(clusterID, regStore)

	d.ProcessEvent(dockerConfigSecret, nil, central.ResourceAction_CREATE_RESOURCE)
	regs, err := regStore.GetPullSecretRegistries(fakeImage, fakeNamespace, nil)
	assert.NotNil(t, regs)
	assert.NoError(t, err)
	require.Len(t, regs, 1)
	assert.Equal(t, regs[0].Config(context.Background()).Username, "hello")

	d.ProcessEvent(dockerConfigSecret, nil, central.ResourceAction_REMOVE_RESOURCE)
	regs, err = regStore.GetPullSecretRegistries(fakeImage, fakeNamespace, nil)
	assert.Nil(t, regs)
	assert.NoError(t, err)

	// Repeat the remove action to ensure no breakage when there is nothing to delete.
	d.ProcessEvent(dockerConfigSecret, nil, central.ResourceAction_REMOVE_RESOURCE)
	regs, err = regStore.GetPullSecretRegistries(fakeImage, fakeNamespace, nil)
	assert.Nil(t, regs)
	assert.NoError(t, err)
}

// TestSAAnnotationImageIntegrationEvents tests that image integration events
// are not generated for secrets that contain a service account annotation
func TestSAAnnotationImageIntegrationEvents(t *testing.T) {
	defaultSA := "default"
	regStore := registry.NewRegistryStore(alwaysInsecureCheckTLS)
	d := newSecretDispatcher(clusterID, regStore)

	// a secret w/ the `default` k8s sa annotation should trigger no imageintegration events
	secret := openshift4xDockerConfigSecret.DeepCopy()
	secret.Annotations[ocpSAAnnotations[0]] = defaultSA
	events := d.ProcessEvent(secret, nil, central.ResourceAction_SYNC_RESOURCE)
	iiEvents := getImageIntegrationEvents(events)
	assert.Len(t, iiEvents, 0)

	// a secret w/ the `default` OCP sa annotation should trigger no imageintegration events
	delete(secret.Annotations, ocpSAAnnotations[0])
	secret.Annotations[ocpSAAnnotations[1]] = defaultSA
	events = d.ProcessEvent(secret, nil, central.ResourceAction_SYNC_RESOURCE)
	iiEvents = getImageIntegrationEvents(events)
	assert.Len(t, iiEvents, 0)
	delete(secret.Annotations, ocpSAAnnotations[1])

	// a secret w/ any sa annotation should trigger no imageintegration events
	secret.Annotations[ocpSAAnnotations[0]] = "blah"
	events = d.ProcessEvent(secret, nil, central.ResourceAction_SYNC_RESOURCE)
	iiEvents = getImageIntegrationEvents(events)
	assert.Len(t, iiEvents, 0)

	// a secret w/ an empty sa annotation should trigger an imageintegration event
	secret.Annotations[ocpSAAnnotations[0]] = ""
	events = d.ProcessEvent(secret, nil, central.ResourceAction_SYNC_RESOURCE)
	iiEvents = getImageIntegrationEvents(events)
	assert.Len(t, iiEvents, 1)

	// a secret w/ no sa annotation should trigger an imageintegration event
	delete(secret.Annotations, ocpSAAnnotations[0])
	events = d.ProcessEvent(secret, nil, central.ResourceAction_SYNC_RESOURCE)
	iiEvents = getImageIntegrationEvents(events)
	assert.Len(t, iiEvents, 1)
}

func getImageIntegrationEvents(events *component.ResourceEvent) []*central.SensorEvent_ImageIntegration {
	var iiEvents []*central.SensorEvent_ImageIntegration
	for _, e := range events.ForwardMessages {
		msg, ok := e.GetResource().(*central.SensorEvent_ImageIntegration)
		if ok {
			iiEvents = append(iiEvents, msg)
		}
	}

	return iiEvents
}

// TestSkipIntegrationCreate verifies that integrations are NOT created in
// various condition.
func TestSkipIntegrationCreate(t *testing.T) {
	globalPullSecret := &v1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      openshift.GlobalPullSecretName,
			Namespace: openshift.GlobalPullSecretNamespace,
		},
	}
	globalPullSecretDupe := &v1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      clusterImgRegistryOperatorSecretName,
			Namespace: clusterImgRegistryOperatorNamespace,
		},
	}
	someOtherSecret := &v1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "stackrox",
			Namespace: "stackrox",
		},
	}

	t.Run("do not skip when all relevant features enabled", func(t *testing.T) {
		testutils.MustUpdateFeature(t, features.SourcedAutogeneratedIntegrations, true)
		t.Setenv(env.AutogenerateGlobalPullSecRegistries.EnvVar(), strconv.FormatBool(true))

		assert.False(t, skipIntegrationCreate(globalPullSecret))
		assert.False(t, skipIntegrationCreate(globalPullSecretDupe))
		assert.False(t, skipIntegrationCreate(someOtherSecret))
	})

	t.Run("do not skip when all relevant features disabled", func(t *testing.T) {
		testutils.MustUpdateFeature(t, features.SourcedAutogeneratedIntegrations, false)
		t.Setenv(env.AutogenerateGlobalPullSecRegistries.EnvVar(), strconv.FormatBool(false))

		assert.False(t, skipIntegrationCreate(globalPullSecret))
		assert.False(t, skipIntegrationCreate(globalPullSecretDupe))
		assert.False(t, skipIntegrationCreate(someOtherSecret))
	})

	t.Run("do not skip when only the sourced feature enabled", func(t *testing.T) {
		testutils.MustUpdateFeature(t, features.SourcedAutogeneratedIntegrations, true)
		t.Setenv(env.AutogenerateGlobalPullSecRegistries.EnvVar(), strconv.FormatBool(false))

		assert.False(t, skipIntegrationCreate(globalPullSecret))
		assert.False(t, skipIntegrationCreate(globalPullSecretDupe))
		assert.False(t, skipIntegrationCreate(someOtherSecret))
	})

	t.Run("skip dupe when sourced feature disabled and global pull feature enabled", func(t *testing.T) {
		testutils.MustUpdateFeature(t, features.SourcedAutogeneratedIntegrations, false)
		t.Setenv(env.AutogenerateGlobalPullSecRegistries.EnvVar(), strconv.FormatBool(true))

		assert.False(t, skipIntegrationCreate(globalPullSecret))
		assert.True(t, skipIntegrationCreate(globalPullSecretDupe))
		assert.False(t, skipIntegrationCreate(someOtherSecret))
	})
}

// parseSecretYAML parses YAML into a Kubernetes *v1.Secret object.
func parseSecretYAML(t *testing.T, yamlData []byte) *v1.Secret {
	var secret v1.Secret
	err := yaml.Unmarshal(yamlData, &secret)
	require.NoError(t, err)
	return &secret
}

func buildSecretYaml(base64data string) []byte {
	return []byte(`
apiVersion: v1
data:
  .dockercfg: ` + base64data + `
kind: Secret
metadata:
  name: secret1
type: kubernetes.io/dockercfg
`)
}

func Test_secretDispatcher_processDockerConfigEvent(t *testing.T) {
	tests := map[string]struct {
		secretData    string
		action        central.ResourceAction
		wantNumEvents int
	}{
		"bad secret with non-utf auth (nu\\x00Op\\x07pZQ)": {
			secretData:    `{"example.com":{"auth":"nullOp7pZQ=="}}`,
			action:        central.ResourceAction_CREATE_RESOURCE,
			wantNumEvents: 0,
		},
		"secret containing one correct and one broken auth entry": {
			secretData:    `{"bad.example.com":{"auth":"nullOp7pZQ=="}, "good.example.com":{"auth":"dXNlcjE6cXdmcGI="}}`,
			action:        central.ResourceAction_CREATE_RESOURCE,
			wantNumEvents: 2, // 1 secret + 1 image integration
		},
		"bad secret with non-utf in auth (user\\xc5name)": {
			secretData:    `{"example.com":{"auth":"dXNlcspuYW1lOnBhc3N3b3Jk"}}`,
			action:        central.ResourceAction_CREATE_RESOURCE,
			wantNumEvents: 0,
		},
		"good secret": {
			secretData:    `{"auths":{"example.com":{"auth":"dXNlcjE6cXdmcGI="}}}`,
			action:        central.ResourceAction_CREATE_RESOURCE,
			wantNumEvents: 2, // 1 secret + 1 image integration
		},
		"secret containing two good registries": {
			secretData:    `{"1.example.com":{"auth":"dXNlcjE6cXdmcGI="}, "2.example.com":{"auth":"dXNlcjE6cXdmcGI="}}`,
			action:        central.ResourceAction_CREATE_RESOURCE,
			wantNumEvents: 3, // 1 secret + 2 image integrations
		},
	}
	for name, tt := range tests {
		t.Run(name, func(t *testing.T) {
			s := newSecretDispatcher(clusterID, registry.NewRegistryStore(func(ctx context.Context, origAddr string) (bool, error) {
				return true, nil
			}))
			secret := parseSecretYAML(t, buildSecretYaml(base64.StdEncoding.EncodeToString([]byte(tt.secretData))))
			got := s.processDockerConfigEvent(secret, nil, tt.action)
			if tt.wantNumEvents == 0 {
				assert.Nil(t, got)
			} else {
				assert.Len(t, got.ForwardMessages, tt.wantNumEvents)
			}
		})
	}
}
